超簡単 Google App Engineで始めるWebアプリケーション 〜リクエスト分割機能がすごかった〜
どうも、GCPヨチヨチ歩きの城岸です。
本日は、Google App Engine(以下GAE)を使って簡単にWebアプリケーションをデプロイする方法を紹介したいと思います。 アプリケーションのバージョンごとにリクエストを分割できる機能が素敵すぎてブログを書かずにはいられませんでした!
それではいってみましょう!
GAEとは
GCP (Google Cloud Platform)が提供するPaaSです。Java、Node.js、Python、Go など好きな言語で作成したアプリケーションをGCPが管理するインフラに簡単にデプロイすることができます。AWSのAWS Elastic Beanstalk
と同じカテゴリのサービスです。
詳細は公式ドキュメントをご覧ください。
やってみた
公式のチュートリアルをベースに進めていきます。
アプリケーションはGo 1.11
で作成します。
検証環境
ブラウザベースのコマンドラインCloudShellを利用します。
(AWSにもこの機能欲しい。。) gcloud
のバージョンは以下の通りです。
cloudshell:~$ gcloud -v Google Cloud SDK 246.0.0 alpha 2019.02.22 app-engine-go app-engine-java 1.9.74 app-engine-php " " app-engine-python 1.9.85 app-engine-python-extras 1.9.74 beta 2019.02.22 bq 2.0.43 cbt cloud-build-local cloud-datastore-emulator 2.1.0 core 2019.05.10 datalab 20190116 docker-credential-gcr gcd-emulator v1beta3-1.0.0 gsutil 4.38 kubectl 2019.05.10 pubsub-emulator 2019.04.26
CloudShell起動
GCPにログイン後、CloudShellを起動します。
以降のシェルはCloudShell上での実行結果です。
プロジェクト作成
プロジェクトを作成します。AWSにはない概念ですが、プロジェクトという単位でシステムを管理します。
$ gcloud projects create blog-project-20190519 --set-as-default Create in progress for [https://cloudresourcemanager.googleapis.com/v1/projects/blog-project-20190519]. Waiting for [operations/cp.XXXXXXXXXXXXXXXXXXXXX] to finish...done.
GAE作成
先ほど作成したプロジェクトにGAEを作成します。リージョンは東京リージョン
とします。
$ gcloud app create --project=blog-project-20190519 You are creating an app for project [blog-project-20190519]. WARNING: Creating an App Engine application for a project is irreversible and the region cannot be changed. More information about regions is at <https://cloud.google.com/appengine/docs/locations>. Please choose the region where you want your App Engine application located: [1] asia-east2 (supports standard and flexible) [2] asia-northeast1 (supports standard and flexible) [3] asia-northeast2 (supports standard and flexible) [4] asia-south1 (supports standard and flexible) [5] australia-southeast1 (supports standard and flexible) [6] europe-west (supports standard and flexible) [7] europe-west2 (supports standard and flexible) [8] europe-west3 (supports standard and flexible) [9] europe-west6 (supports standard and flexible) [10] northamerica-northeast1 (supports standard and flexible) [11] southamerica-east1 (supports standard and flexible) [12] us-central (supports standard and flexible) [13] us-east1 (supports standard and flexible) [14] us-east4 (supports standard and flexible) [15] us-west2 (supports standard and flexible) [16] cancel Please enter your numeric choice: 2 Creating App Engine application in project [blog-project-20190519] and region [asia-northeast1]....done. Success! The app is now created. Please use `gcloud app deploy` to deploy your first app. To take a quick anonymous survey, run: $ gcloud alpha survey
請求アカウント設定
GAEを利用するため、プロジェクトに対しての請求を有効にします。
サンプルアプリケーションダウンロード
次のコマンドを実行しサンプルアプリケーションをダウンロードします。
$ git clone https://github.com/GoogleCloudPlatform/golang-samples.git $ cd golang-samples/appengine/go11x/helloworld
以下のファイルがダウンロードされます。
$ tree . ├── app.yaml ├── helloworld.go └── helloworld_test.go 0 directories, 3 files
app.yaml
には、ランタイムが指定されています。
# Copyright 2019 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. runtime: go111
helloworld.go
は、httpリクエストに対し"Hello, World!"
という文字列を返却するアプリケーションとなっています。
// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // [START gae_go111_app] // Sample helloworld is an App Engine app. package main // [START import] import ( "fmt" "log" "net/http" "os" ) // [END import] // [START main_func] func main() { http.HandleFunc("/", indexHandler) // [START setting_port] port := os.Getenv("PORT") if port == "" { port = "8080" log.Printf("Defaulting to port %s", port) } log.Printf("Listening on port %s", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) // [END setting_port] } // [END main_func] // [START indexHandler] // indexHandler responds to requests with our greeting. func indexHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } fmt.Fprint(w, "Hello, World!") } // [END indexHandler] // [END gae_go111_app]
helloworld_test.go
は、簡単なテストコードです。
// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "net/http" "net/http/httptest" "testing" ) func TestIndexHandler(t *testing.T) { req, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() handler := http.HandlerFunc(indexHandler) handler.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusOK { t.Errorf( "unexpected status: got (%v) want (%v)", status, http.StatusOK, ) } expected := "Hello, World!" if rr.Body.String() != expected { t.Errorf( "unexpected body: got (%v) want (%v)", rr.Body.String(), "Hello, World!", ) } } func TestIndexHandlerNotFound(t *testing.T) { req, err := http.NewRequest("GET", "/404", nil) if err != nil { t.Fatal(err) } rr := httptest.NewRecorder() handler := http.HandlerFunc(indexHandler) handler.ServeHTTP(rr, req) if status := rr.Code; status != http.StatusNotFound { t.Errorf( "unexpected status: got (%v) want (%v)", status, http.StatusOK, ) } }
サンプルアプリケーションデプロイ
サンプルアプリケーションをデプロイします。
$ gcloud app deploy Services to deploy: descriptor: [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld/app.yaml] source: [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld] target project: [blog-project-20190519] target service: [default] target version: [20190519t083733] target url: [https://blog-project-20190519.appspot.com] Do you want to continue (Y/n)? Y Beginning deployment of service [default]... Created .gcloudignore file. See `gcloud topic gcloudignore` for details. ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 3 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://blog-project-20190519.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
target url
にアクセスしてみましょう!
$ curl https://blog-project-20190519.appspot.com/ Hello, World!
Hello, World!
という文字列が表示されました。
デプロイはこれだけ。これだけでGCPが管理するインフラに自身のアプリケーションをデプロイすることができます!簡単ですね!
サンプルアプリケーション変更
出力される文字列をHello, World!
からHello, World!!!
に変更してみましょう。
// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // [START gae_go111_app] // Sample helloworld is an App Engine app. package main // [START import] import ( "fmt" "log" "net/http" "os" ) // [END import] // [START main_func] func main() { http.HandleFunc("/", indexHandler) // [START setting_port] port := os.Getenv("PORT") if port == "" { port = "8080" log.Printf("Defaulting to port %s", port) } log.Printf("Listening on port %s", port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) // [END setting_port] } // [END main_func] // [START indexHandler] // indexHandler responds to requests with our greeting. func indexHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } fmt.Fprint(w, "Hello, World!!!") } // [END indexHandler] // [END gae_go111_app]
テストコードも同様に変更しテストを実行します。
$ go test PASS ok _/home/jogan_naoki/golang-samples/appengine/go11x/helloworld 0.004s
サンプルアプリケーション再デプロイ
サンプルアプリケーションを再デプロイします。
$ gcloud app deploy Services to deploy: descriptor: [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld/app.yaml] source: [/home/jogan_naoki/golang-samples/appengine/go11x/helloworld] target project: [blog-project-20190519] target service: [default] target version: [20190519t085640] target url: [https://blog-project-20190519.appspot.com] Do you want to continue (Y/n)? Y Beginning deployment of service [default]... ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 2 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...done. Setting traffic split for service [default]...done. Deployed service [default] to [https://blog-project-20190519.appspot.com] You can stream logs from the command line by running: $ gcloud app logs tail -s default To view your application in the web browser run: $ gcloud app browse
もう一度target url
にアクセスしてみましょう!
$ curl https://blog-project-20190519.appspot.com/ Hello, World!!!
変更が反映されました!
トラフィック分割
ここからが一番伝えたかったところです。
先ほどのアプリケーションはバーション管理されています。そしてどのバージョンにどれくらいトラフィックが流れているかを確認することができます。
今は再デプロイしたアプリケーションに100%
のトラフィックが流れています。古いバージョンにもトラフィックを流してみましょう。「トラフィックを分割」をクリックします。
「トラフィック分割の基準」、「トラフィック割り当て」を指定し、「保存」をクリックします。
トラフィックが分割されました。(これだけでそんな高度なことができるだと!?
対象のURLにアクセスしトラフィックが分割されていることを確認してみましょう!
$ curl https://blog-project-20190519.appspot.com/ Hello, World! $ curl https://blog-project-20190519.appspot.com/ Hello, World!!! $ curl https://blog-project-20190519.appspot.com/ Hello, World! $ curl https://blog-project-20190519.appspot.com/ Hello, World! $ curl https://blog-project-20190519.appspot.com/ Hello, World!!!
ちゃんと分割されていることが確認できます!これでA/Bテストも楽々
トラフィック移行
もちろんバージョンを指定して全てのトラフィックを移行することもできます。
あっ、やばい!バージョン戻したい!なんて時には非常に便利ですね。
プロジェクトの削除
最後はdeleteコマンドでリソースを削除します。
$ gcloud projects delete blog-project-20190519 Your project will be deleted. Do you want to continue (Y/n)? Y Deleted [https://cloudresourcemanager.googleapis.com/v1/projects/blog-project-20190519]. You can undo this operation for a limited period by running: $ gcloud projects undelete blog-project-20190519 To take a quick anonymous survey, run: $ gcloud alpha survey
プロジェクトを削除した時点で課金は停止されます。が、30日間(復元期間中)であれば削除したプロジェクトは復元可能です。なんとお優しい!
さいごに
GAEを使って簡単にWebアプリケーションをデプロイする方法を紹介しました。GAEのスケールなどまだまだ紹介しきれていない機能があるので、これからもブログを書いていこうと思います!!
また、エンジニアとしてGCPで何ができるのか?を知っておくことはとても重要なことであると感じました。その上で自身のアプリケーションに最適なクラウド(AWS/GCP/Azureなど)を選択したいものですね!
AWSがいいなと感じた場合は、弊社メンバーズをご検討ください!